ποΈGitΠ―ΡΠ°ποΈ
docs/en/developer/measurement.md 4d4070c8e1ed73a56ee2f054300a0f62e3fe2e62 (4d4070c8) Text, 6.60 KB
title: Measurement & Formatting
parent: Developer Guide
nav_order: 9
last_updated: 2026-08-19
aliases:
β’ measurement
β’ metric-formatter
β’ number-formatter
Measurement & Formatting
How the Meshtastic Android/KMP app formats numbers, units, and locale-sensitive values.
Overview
All measurement data transmitted by Meshtastic radios uses metric units (meters, Β°C, hPa, m/s, etc.). The app converts and formats these values for display using two core utilities:
βββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββ
β Utility β Location β Purpose β
βββββββββββββββββββΌββββββββββββββββββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββββββ€
β T383838MetricFormatter β T383838core/common/.../util/MetricFormatter.kt β Converts and formats physical measuβ¦ β
β T383838NumberFormatter β T383838core/common/.../util/NumberFormatter.kt β Low-level fixed-point number formatβ¦ β
βββββββββββββββββββ΄ββββββββββββββββββββββββββββββββββββββββββ΄βββββββββββββββββββββββββββββββββββββββ
Both live in T383838org.meshtastic.core.common.util and are available to all KMP targets (Android, Desktop, iOS).
MetricFormatter API
T383838MetricFormatter is a Kotlin T383838object with pure functions for each measurement type:
T282828
Tff7b72object T56d364MetricFormatter Tb4b4b4{
Tff7b72fun Td2a8fftemperatureTb4b4b4(Te6edf3celsiusTb4b4b4: Tffa657FloatTb4b4b4, Te6edf3isFahrenheitTb4b4b4: Tffa657BooleanTb4b4b4)Tb4b4b4: Tffa657String
Tff7b72fun Td2a8ffvoltageTb4b4b4(Te6edf3voltsTb4b4b4: Tffa657FloatTb4b4b4, Te6edf3decimalPlacesTb4b4b4: Tffa657Int Tff7b72= T79c0ff2Tb4b4b4)Tb4b4b4: Tffa657String
Tff7b72fun Td2a8ffcurrentTb4b4b4(Te6edf3milliAmpsTb4b4b4: Tffa657FloatTb4b4b4, Te6edf3decimalPlacesTb4b4b4: Tffa657Int Tff7b72= T79c0ff1Tb4b4b4)Tb4b4b4: Tffa657String
Tff7b72fun Td2a8ffpercentTb4b4b4(Te6edf3valueTb4b4b4: Tffa657FloatTb4b4b4, Te6edf3decimalPlacesTb4b4b4: Tffa657Int Tff7b72= T79c0ff1Tb4b4b4)Tb4b4b4: Tffa657String
Tff7b72fun Td2a8ffhumidityTb4b4b4(Te6edf3valueTb4b4b4: Tffa657FloatTb4b4b4)Tb4b4b4: Tffa657String
Tff7b72fun Td2a8ffpressureTb4b4b4(Te6edf3hPaTb4b4b4: Tffa657FloatTb4b4b4, Te6edf3decimalPlacesTb4b4b4: Tffa657Int Tff7b72= T79c0ff1Tb4b4b4)Tb4b4b4: Tffa657String
Tff7b72fun Td2a8ffsnrTb4b4b4(Te6edf3valueTb4b4b4: Tffa657FloatTb4b4b4, Te6edf3decimalPlacesTb4b4b4: Tffa657Int Tff7b72= T79c0ff1Tb4b4b4)Tb4b4b4: Tffa657String
Tff7b72fun Td2a8ffrssiTb4b4b4(Te6edf3valueTb4b4b4: Tffa657IntTb4b4b4)Tb4b4b4: Tffa657String
Tff7b72fun Td2a8ffwindSpeedTb4b4b4(Te6edf3metersPerSecondTb4b4b4: Tffa657FloatTb4b4b4, Te6edf3isImperialTb4b4b4: Tffa657BooleanTb4b4b4, Te6edf3decimalPlacesTb4b4b4: Tffa657Int Tff7b72= T79c0ff1Tb4b4b4)Tb4b4b4: Tffa657String
Tff7b72fun Td2a8ffrainfallTb4b4b4(Te6edf3millimetersTb4b4b4: Tffa657FloatTb4b4b4, Te6edf3isImperialTb4b4b4: Tffa657BooleanTb4b4b4, Te6edf3decimalPlacesTb4b4b4: Tffa657Int Tff7b72= T79c0ff1Tb4b4b4)Tb4b4b4: Tffa657String
Tb4b4b4}
Usage
T282828
T8b949e// Temperature β Fahrenheit conversion is handled automatically
Te6edf3MetricFormatterTb4b4b4.Te6edf3temperatureTb4b4b4(T79c0ff2T79c0ff2.5fTb4b4b4, Te6edf3isFahrenheit Tff7b72= Tff7b72trueTb4b4b4) T8b949e// "72.5Β°F"
Te6edf3MetricFormatterTb4b4b4.Te6edf3temperatureTb4b4b4(T79c0ff2T79c0ff2.5fTb4b4b4, Te6edf3isFahrenheit Tff7b72= Tff7b72falseTb4b4b4) T8b949e// "22.5Β°C"
T8b949e// Signal metrics
Te6edf3MetricFormatterTb4b4b4.Te6edf3snrTb4b4b4(Tff7b72-T79c0ff5.2fTb4b4b4) T8b949e// "-5.2 dB"
Te6edf3MetricFormatterTb4b4b4.Te6edf3rssiTb4b4b4(Tff7b72-T79c0ff9T79c0ff7Tb4b4b4) T8b949e// "-97 dBm"
T8b949e// Environment
Te6edf3MetricFormatterTb4b4b4.Te6edf3pressureTb4b4b4(T79c0ff1T79c0ff0T79c0ff1T79c0ff3.25fTb4b4b4) T8b949e// "1013.3 hPa"
Te6edf3MetricFormatterTb4b4b4.Te6edf3humidityTb4b4b4(T79c0ff6T79c0ff5.0fTb4b4b4) T8b949e// "65%"
Te6edf3MetricFormatterTb4b4b4.Te6edf3windSpeedTb4b4b4(T79c0ff3.7fTb4b4b4, Te6edf3isImperial Tff7b72= Tff7b72falseTb4b4b4) T8b949e// "3.7 m/s"
Te6edf3MetricFormatterTb4b4b4.Te6edf3windSpeedTb4b4b4(T79c0ff3.7fTb4b4b4, Te6edf3isImperial Tff7b72= Tff7b72trueTb4b4b4) T8b949e// "8.3 mph"
Te6edf3MetricFormatterTb4b4b4.Te6edf3rainfallTb4b4b4(T79c0ff1T79c0ff2.3fTb4b4b4, Te6edf3isImperial Tff7b72= Tff7b72falseTb4b4b4) T8b949e// "12.3 mm"
Te6edf3MetricFormatterTb4b4b4.Te6edf3rainfallTb4b4b4(T79c0ff1T79c0ff2.3fTb4b4b4, Te6edf3isImperial Tff7b72= Tff7b72trueTb4b4b4) T8b949e// "0.5 in"
T8b949e// Power
Te6edf3MetricFormatterTb4b4b4.Te6edf3voltageTb4b4b4(T79c0ff3.95fTb4b4b4) T8b949e// "3.95 V"
Te6edf3MetricFormatterTb4b4b4.Te6edf3currentTb4b4b4(T79c0ff1T79c0ff2T79c0ff5.0fTb4b4b4) T8b949e// "125.0 mA"
NumberFormatter
T383838NumberFormatter provides locale-independent decimal formatting using pure arithmetic (no T383838String.format or T383838DecimalFormat):
T282828
Tff7b72object T56d364NumberFormatter Tb4b4b4{
Tff7b72fun Td2a8ffformatTb4b4b4(Te6edf3valueTb4b4b4: Tffa657DoubleTb4b4b4, Te6edf3decimalPlacesTb4b4b4: Tffa657IntTb4b4b4)Tb4b4b4: Tffa657String
Tff7b72fun Td2a8ffformatTb4b4b4(Te6edf3valueTb4b4b4: Tffa657FloatTb4b4b4, Te6edf3decimalPlacesTb4b4b4: Tffa657IntTb4b4b4)Tb4b4b4: Tffa657String
Tb4b4b4}
β Why locale-independent? Meshtastic is a mesh networking app where consistency matters β sensor
β readings shared between nodes should look the same everywhere. T383838NumberFormatter always uses T383838. as
β the decimal separator.
Unit Conversion
Three measurements convert away from metric for display, each gated by a boolean flag sourced from the user's device locale or preferences:
βββββββββββββββ¬βββββββββββββββ¬βββββββββββββββββββββββββββββββ¬ββββββββββββββββββββββ
β Measurement β Flag β Source β Conversion β
βββββββββββββββΌβββββββββββββββΌβββββββββββββββββββββββββββββββΌββββββββββββββββββββββ€
β T383838temperature β T383838isFahrenheit β T383838getSystemTemperatureUnit() β T383838Β°F = Β°C Γ 1.8 + 32 β
β T383838windSpeed β T383838isImperial β T383838getSystemMeasurementSystem() β m/s Γ 2.23694 β mph β
β T383838rainfall β T383838isImperial β T383838getSystemMeasurementSystem() β mm Γ· 25.4 β in β
βββββββββββββββ΄βββββββββββββββ΄βββββββββββββββββββββββββββββββ΄ββββββββββββββββββββββ
The two source functions (in T383838core/common/.../util/MeasurementSystem.kt) are deliberately separate: some locales mix systems (the UK uses miles for distance but Celsius for temperature), so temperature must never be derived from the distance unit. On Android, T383838getSystemTemperatureUnit() delegates to T383838androidx.core.text.util.LocalePreferences, which resolves CLDR locale data and honors the Android 14+ Regional preferences temperature override.
Everything else (voltage, current, pressure, SNR, RSSI, humidity, percent) displays in its native metric units. The user-facing Units & Locale page explains what end users see.
Adding a New Measurement Type
To add a new measurement formatter:
1. Add a function to T383838MetricFormatter in T383838core/common/src/commonMain/kotlin/org/meshtastic/core/common/util/MetricFormatter.kt:
T282828
Tff7b72fun Td2a8ffradiationTb4b4b4(Te6edf3microSievertsTb4b4b4: Tffa657FloatTb4b4b4, Te6edf3decimalPlacesTb4b4b4: Tffa657Int Tff7b72= T79c0ff2Tb4b4b4)Tb4b4b4: Tffa657String Tff7b72=
Ta5d6ff"Tffd700${Te6edf3NumberFormatterTb4b4b4.Te6edf3formatTb4b4b4(Te6edf3microSievertsTb4b4b4, Te6edf3decimalPlacesTb4b4b4)Tffd700}Ta5d6ff ΞΌSv/hTa5d6ff"
2. Add tests in T383838core/common/src/commonTest/:
T282828
Tf0883e@Test
Tff7b72fun Td2a8ffradiationFormattingTb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3assertEqualsTb4b4b4(Ta5d6ff"Ta5d6ff0.15 ΞΌSv/hTa5d6ff"Tb4b4b4, Te6edf3MetricFormatterTb4b4b4.Te6edf3radiationTb4b4b4(T79c0ff0.15fTb4b4b4)Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Ta5d6ff"Ta5d6ff1.23 ΞΌSv/hTa5d6ff"Tb4b4b4, Te6edf3MetricFormatterTb4b4b4.Te6edf3radiationTb4b4b4(T79c0ff1.234fTb4b4b4)Tb4b4b4)
Tb4b4b4}
3. Use in UI β call from any T383838commonMain composable or ViewModel:
T282828
Te6edf3TextTb4b4b4(Te6edf3text Tff7b72= Te6edf3MetricFormatterTb4b4b4.Te6edf3radiationTb4b4b4(Te6edf3nodeTb4b4b4.Te6edf3radiationLevelTb4b4b4)Tb4b4b4)
4. Run verification:
T282828
./gradlew :core:common:allTests
DateFormatter
Date and time formatting uses the T383838DateFormatter T383838expect object with platform-specific T383838actual implementations:
βββββββββββββββββββββββββββ¬βββββββββββββββββββββββββ
β Function β Output Example β
βββββββββββββββββββββββββββΌβββββββββββββββββββββββββ€
β T383838formatRelativeTime() β "5 min ago" β
β T383838formatDateTime() β "May 13, 2026 2:30 PM" β
β T383838formatShortDate() β "May 13" β
β T383838formatTime() β "2:30 PM" β
β T383838formatTimeWithSeconds() β "2:30:45 PM" β
β T383838formatDate() β "2026-05-13" β
β T383838formatDateTimeShort() β "5/13/26 2:30 PM" β
βββββββββββββββββββββββββββ΄βββββββββββββββββββββββββ
Unlike T383838MetricFormatter, T383838DateFormatter is declared with T383838expect/T383838actual (an T383838expect object in T383838commonMain, an T383838actual object per platform) because date formatting inherently depends on platform locale APIs.
Design Decisions
ββββββββββββββββββββββββββββββββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββββββββββ
β Decision β Rationale β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββΌββββββββββββββββββββββββββββββββββββββββββββββ€
β Locale-independent decimal separator (T383838.) β Mesh data shared between nodes must be conβ¦ β
β Pure arithmetic formatting (no T383838DecimalFormat) β Works identically on JVM, Native, and JS tβ¦ β
β Only temperature, wind speed, and rainfall convert β The remaining metric units are universallyβ¦ β
β T383838object singleton pattern β Stateless utility β no instance managementβ¦ β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββ΄ββββββββββββββββββββββββββββββββββββββββββββββ
Related
β’ User-facing docs: Units & Locale explains what end users see
β’ Source code: T383838core/common/src/commonMain/kotlin/org/meshtastic/core/common/util/MetricFormatter.kt
β’ Tests: T383838core/common/src/commonTest/kotlin/org/meshtastic/core/common/util/MetricFormatterTest.kt
Served by rngit 1.5.0 - Generated in 0.06s